[6.x] Image transparency checkerboard#13975
Conversation
|
It might be worthwhile looking into calculating this upon upload and stored in the meta vs on the fly in browser. It could be something as simple as: use Intervention\Image\ImageManagerStatic as Image;
$image = Image::make($path)->resize(1, 1, function ($constraint) {
$constraint->aspectRatio();
});
$averageColor = $image->pickColor(0, 0); // [R, G, B, A]
// Normalized to 0-1
$meanR = $averageColor[0] / 255;
$meanG = $averageColor[1] / 255;
$meanB = $averageColor[2] / 255;
$luminance = (0.2126 * $meanR) + (0.7152 * $meanG) + (0.0722 * $meanB);
$isBright = $luminance > 0.5;(not tested AI generated - but looks correct) |
|
The brightness/luminance would indeed be interesting on the frontend as well, now that everything is glass and backdrop filters :) With the additional benefit of front-loading the calculation at the time of upload. |
…r uses the server-provided tone directly, falling back to client-side Canvas detection only for SVGs.
… with a transparent background, so it can skip transparent pixels and only measure the actual content. This works independently of the user's configured image driver -- if the Imagick PHP extension is installed, SVGs get tone detection; if not, they get null gracefully. Raster image detection is unchanged (still uses Intervention Image).
|
OK, I've given it a go! I've updated the PR description to note what's happening.
You could use it like this: {{ if asset:tone == "light" }}
<div class="preview preview-dark">
{{ elseif asset:tone == "dark" }}
<div class="preview preview-light">
{{ /if }}
<img src="{{ asset:url }}" alt="" />
</div>Or with the booleans: {{ if asset:is_light_tone }}
<div class="bg-dark">…</div>
{{ elseif asset:is_dark_tone }}
<div class="bg-light">…</div>
{{ /if }} |
|
This is really nice. Great solution to a tricky situation. |
|
Did you push up the antlers stuff? I don't see anything. |
… button to show transparency
when the Imagick branch finds zero non-transparent pixels (meaning it couldn't meaningfully rasterize the SVG), it now falls through to the XML color-parsing fallback instead of returning null. Same for when Imagick throws an exception.
|
@jasonvarga the Antlers syntax should "just work" because we're referencing saved meta data. Here's some Antlers and the browser rendering it in the background: |
…Vue file when server generation has not taken place yet
…nch and will come back via merge.
# Conflicts: # resources/js/components/assets/Editor/Editor.vue # src/Imaging/Attributes.php
jasonvarga
left a comment
There was a problem hiding this comment.
I've resolved the merge conflicts for you.
This looks almost done but something I noticed was that the tone isn't properly picked up for transparent PNGs.
✅ The black HBO logo SVG gets correctly picked up as "dark".
❌ The black HBO logo PNG gets picked up as "light".
❌ The black dog PNG gets picked up as "light".
🤷♂️ The golden dog PNG gets correctly picked up as "light", but this might just be by fluke.
✅ JPGs work fine. (not in this screenshot)
|
@jasonvarga Are you using |
|
I think you could solve the transparent issue by ignoring transparent pixels below a certain threshold. Then for any semi-transparent pixels doing a double comparison of blending the transparent pixels with white and black and comparing to see which yields a higher contrast: $sum_white = 0.0;
$sum_black = 0.0;
$count = 0;
// Sample ~256 pixels for speed
$step = max(1, (int) ceil(($w * $h) / 256));
$i = 0;
for ($y = 0; $y < $h; $y++) {
for ($x = 0; $x < $w; $x++) {
if ($i++ % $step !== 0) {
continue;
}
$color = $image->pickColor($x, $y);
// Intervention Image v3: alpha is 0.0 (transparent) to 1.0 (opaque)
$alpha = $color->alpha()->toFloat(); // or ->value() / ->float() depending on exact version
if ($alpha < 0.02) { // skip fully/nearly transparent pixels
continue;
}
// Colors are already 0–1 in Intervention v3
$r = $color->red()->toFloat();
$g = $color->green()->toFloat();
$b = $color->blue()->toFloat();
// Blend against WHITE background (1,1,1)
$r_white = $r * $alpha + (1 - $alpha) * 1.0;
$g_white = $g * $alpha + (1 - $alpha) * 1.0;
$b_white = $b * $alpha + (1 - $alpha) * 1.0;
$l_white = 0.299 * $r_white + 0.587 * $g_white + 0.114 * $b_white;
// Blend against BLACK background (0,0,0)
$r_black = $r * $alpha + (1 - $alpha) * 0.0;
$g_black = $g * $alpha + (1 - $alpha) * 0.0;
$b_black = $b * $alpha + (1 - $alpha) * 0.0;
$l_black = 0.299 * $r_black + 0.587 * $g_black + 0.114 * $b_black;
$sum_white += $l_white;
$sum_black += $l_black;
$count++;
}
}
if ($count === 0) {
return null; // fully transparent → no decision possible
}
$avg_white = $sum_white / $count;
$avg_black = $sum_black / $count;
// Option 1: Simple & effective for most logos/icons
// If it looks medium-bright or brighter when on white → recommend dark background
if ($avg_white >= 0.52) { // ← tune this threshold: 0.5–0.6
return 'dark'; // better contrast on black
}
return 'light'; // better/safer on white
// Option 2: More explicit contrast comparison (uncomment if you prefer)
// $contrast_white = max($avg_white, 1 - $avg_white);
// $contrast_black = max($avg_black, 1 - $avg_black);
// return ($contrast_black > $contrast_white) ? 'dark' : 'light';AI generated code - idea is mine :) |
|
Good call guys. 🎉 |
…. Browser preference persisted. Slider to right of toggle in header.
…tive panel header component.
switched to dark/light instead of current/alternate and checking isCpDark. if you pick dark, you get dark. then we dont have to react to theme changes.
…into image-brightness-detection # Conflicts: # resources/css/components/assets.css # resources/js/components/assets/Browser/Grid.vue
…herwise explicitly chosen checkerboard color.
… selected. otherwise the button would look like it does nothing.
|
There's been a bunch of back and forth discussion about this one. We ended up simplifying the approach. We are now just adding adding transparency toggles so that you can manually flick between light, dark, and off. Within listings, "off" will still show the checkerboard matching your UI color mode (light/dark) on hover. |




Description of the Problem
As discussed in #13927, transparent assets can be primarily light or dark.
Under certain conditions, it can be difficult to discern an image against a checkerboard background—for example, when the logo is white or black.
Edit by Jason:
This PR has been greatly simplified. We are now just adding adding transparency toggles so that you can manually flick between light, dark, and off.
Within listings, "off" will still show the checkerboard matching your UI color mode (light/dark) on hover.
Grid view in asset browser:

Assets fieldtype:

I've left the remainder of the initial PR description here for history.
What this PR Does
by sampling pixels using built-in browser APIs (Canvas 2D + Image). This is computed server-side on upload using Intervention Image (works with both GD and Imagick drivers) and stored in the asset's.meta/*.yamlfile alongside existing metadata likewidth,height, andduration.(Updated since initial PR):
How it works
lightordark) is written to meta astone.tone: null.generateMeta()pipeline.Exposed to developers in Antlers templates
{{ tone }}-- returnslight,dark, or null{{ is_light_tone }}/{{ is_dark_tone }}-- boolean helpersWorks the same way as
focus_cssand other meta-driven asset values.Before
(and you'd have a similar problem if you had a dark logo with a dark checkerboard background)
After
White and black logos are now much easier to discern:
(ignore the aspect ratio issue here, that's an existing issue that I'll fix separately)
How to Reproduce
/cp/assetsand upload a transparent white or black logo